﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DDD.Core {

    /// <summary>
    /// キャッシュ アレイ クラス
    /// </summary>
    /// <remarks>
    /// キャッシュ <see cref="Cache{T}"/> を保存するためのコンテナ クラスです。
    /// キー オブジェクトと <typeparamref name="T"/> 型の値（リソース）のペアを1単位として保管します。
    /// オブジェクトの同一性はハッシュ値で判定されます。
    /// キー オブジェクトへの参照がすべて消えGCによって回収されると、リリースイベントの対象となり、
    /// <see cref="ReleaseEventHandler"/> メソッドが呼び出されます。
    /// ハンドラー メソッドではキー オブジェクトの「型」と「値」が参照できるので、
    /// それを使用して例えばOpenGLの非マネージド リソースを解放してください。
    /// オブジェクトのインスタンスには（すでに解放済みなので）アクセスできません。
    /// </remarks>
    /// <typeparam name="T">リソースの型</typeparam>
    /// <seealso cref="Cache{T}"/>
    /// <seealso cref="ReleaseEventHandler"/>
    public class CacheArray<T> : IEnumerable<Cache<T>> where T : struct {


        #region Field
        Dictionary<int, Cache<T>> cache;
        #endregion

        #region Constructor
        /// <summary>
        /// コンストラクター
        /// </summary>
        /// <remarks>
        /// 初期値は以下のとおりです。
        /// <list type="bullet">
        ///   <item>登録済みのキャッシュ = なし</item>
        /// </list>
        /// </remarks>
        public CacheArray () {
            this.cache = new Dictionary<int, Cache<T>> ();
        }
        #endregion

        #region Property
        /// <summary>
        /// <see cref="Object"/> をキャッシュされたリソースに変換するインデクサー
        /// </summary>
        /// <remarks>
        /// 指定のオブジェクト <paramref name="obj"/> （キー）に関連づけられた <typeparamref name="T"/> 型の値（リソース）を返します。
        /// 指定のオブジェクトがキャッシュされていなかった場合、
        /// 規定値(int型なら通常0)が返ります。
        /// <note type="implement">
        /// 見つからなかった場合0を返す仕様は正しいか？
        /// 素直に例外を発生した方が良いような気もする。
        /// </note>
        /// </remarks>
        /// <param name="obj">オブジェクト</param>
        /// <returns>紐づけられたリソース</returns>
        public T this[Object obj] {
            get {
                var hash = obj.GetHashCode ();
                if (!cache.ContainsKey (hash)) {
                    return default (T);
                }
                return cache[hash].Value;
            }
        }

        /// <summary>
        /// リソースの総数
        /// </summary>
        /// <remarks>
        /// 現在キャッシュしているリソースの総数です。
        /// </remarks>
        public int CachedCount {
            get { return cache.Count; }
        }

        /// <summary>
        /// すべてのリソース キャッシュを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// 現在キャッシュしているすべてのリソース キャッシュを列挙します。
        /// </remarks>
        IEnumerator<Cache<T>> IEnumerable<Cache<T>>.GetEnumerator () {
            return cache.Values.GetEnumerator ();
        }

        /// <summary>
        /// すべてのリソース キャッシュを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// 現在キャッシュしているすべてのリソース キャッシュを列挙します。
        /// </remarks>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () {
            return cache.Values.GetEnumerator ();
        }
        #endregion

        #region Method
        /// <summary>
        /// リソースの登録
        /// </summary>
        /// <remarks>
        /// このキャッシュ アレイに指定のリソース <paramref name="value"/> を
        /// <paramref name="obj"/> をキーとして登録します。
        /// すでに登録されているオブジェクトを再登録しようとすると例外を発生します。
        /// オブジェクトの同一性の判定は <see cref="Object.GetHashCode()"/> メソッドで行われます。
        /// </remarks>
        /// <param name="obj">オブジェクト（キー）</param>
        /// <param name="value">リソース（バリュー）</param>
        /// <exception cref="ArgumentNullException"><paramref name="obj"/> が <c>null</c></exception>
        /// <exception cref="ArgumentException">同一のハッシュを持つオブジェクトがすでに登録済み</exception>
        public void Add (Object obj, T value) {
            if (obj == null) {
                throw new ArgumentNullException ("Object is null");
            }
            if (cache.ContainsKey (obj.GetHashCode ())) {
                throw new ArgumentException ("Cache has already same key object");
            }

            this.cache.Add (obj.GetHashCode (), new Cache<T> (obj, value));
        }

        /// <summary>
        /// リソースの削除
        /// </summary>
        /// <remarks>
        /// 指定のオブジェクト <see cref="obj"/> を持つ登録済みのリソースを削除します。
        /// <see cref="obj"/> が登録されていないか、<c>null</c> だった場合は何もしません。
        /// </remarks>
        /// <param name="obj">オブジェクト（キー）</param>
        /// <returns>削除したら <c>true</c>, そうでなければ <c>false</c></returns>
        public bool Remove (Object obj) {
            if (obj == null) {
                return false;
            }
            return this.cache.Remove (obj.GetHashCode ());
        }

        /// <summary>
        /// 不正なオブジェクトをキーとするリソースの解放
        /// </summary>
        /// <remarks>
        /// 登録済みのオブジェクトのうちすべての参照が削除されGCによって回収されたオブジェクトをキーとする
        /// リソースを解放します。リリースの解放では <see cref="ReleaseEvents"/> が呼び出されます。
        ///  <see cref="ReleaseEvents"/> に適切なリリース ハンドラー <see cref="ReleaseEventHandler"/> メソッドを登録する事で、
        ///  ユーザーは例えばOpenGL固有のアンマネージド リソースを解放可能です。
        /// <code>
        ///   foreach 登録済みのすべてのオブジェクト
        ///      if GCによって回収済み
        ///         リリース イベント ハンドラーの呼び出し
        ///         キャッシュから削除
        /// </code>
        /// </remarks>
        /// <seealso cref="ReleaseEvents"/>
        /// <seealso cref="ReleaseHandler"/>
        // (*1) ここでToArray()してるのは「コピー」を作成するため。
        // IEnumerableのままだと次のforeach内部のRemove()の次のイテレーションでエラーになる。
        // 絶対に消さないように！！
        public void Release () {
            var invalids = cache.Where (x => !x.Value.IsValid).ToArray ();   // ...(*1)
            foreach (var res in invalids) {
                if (ReleaseEvents != null) {
                    ReleaseEvents (res.Value.ObjectType, res.Value.Value);
                }
                cache.Remove (res.Key);
            }
        }

        /// <inheritodc/>
        public override string ToString () {
            return string.Format ("Cache: Count={0}", CachedCount);
        }

        #endregion

        #region Event
        /// <summary>
        /// リリース ハンドラー デリゲート
        /// </summary>
        /// <remarks>
        /// リソースの解放のために呼ばれるコールバック メソッドです。
        /// このデリゲートを <see cref="ReleaseEvents"/> に登録する事で、リソース解放処理をユーザーが定義する事が可能です。
        /// 引数の <paramref name="objectType"/> はキーとなったオブジェクトの型です。
        /// <paramref name="value"/> はその時のリソースです。
        /// 既にGCによってオブジェクト インスタンスは解放済みなので、型情報のみが利用可能です。
        /// ユーザーはこのメソッドを適切に実装する事で、例えばOpenGL固有のアンマネージド リソースを解放可能です。
        /// </remarks>
        /// <param name="objectType">解放されたキー オブジェクトの型</param>
        /// <param name="value">リソース</param>
        /// <seealso cref="ReleaseEvents"/>
        public delegate void ReleaseHandler (Type objectType, T value);

        /// <summary>
        /// リリース イベント
        /// </summary>
        /// <remarks>
        /// リソースの解放のために呼ばれるイベントです。
        /// このイベントに登録する事でユーザー固有のリソース解放処理を登録可能です。
        /// </remarks>
        /// <seealso cref="ReleaseHandler"/>
        public event ReleaseHandler ReleaseEvents;

        #endregion

    }

}
